/*
 *  This file is part of Bracket Security
 *  Copyright 2011 David R. Smith
 *
 */
package asia.redact.bracket.security.config;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigInteger;
import java.security.cert.Certificate;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Hashtable;
import java.util.Vector;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.log4j.Logger;
import org.bouncycastle.jce.X509Principal;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.x509.X509V3CertificateGenerator;

import asia.redact.bracket.util.RandomWords;
import asia.redact.bracket.util.StrengthenPasswordWordFilter;

/**
 * <pre>
 * This class does all the required setup to get going. It looks for 
 * the keystore and password files in the documented locations and, if
 * not found, creates them. It even creates an RSA digital certificate.
 * 
 * This class is Thread-safe
 * 
 * </pre>
 * 
 * @author Dave
 *
 */

public class ZeroConfig implements SecurityConfig {
	
	private static final Logger log = Logger.getLogger(ZeroConfig.class);
	
	private String keyPass;
	private String keystorePass;
	private final Lock lock;
	private RandomWords randomWords;
	
	public ZeroConfig() {
		this(new ReentrantLock());
	}
	
	/**
	 * This constructor uses a lock to do its work in a thread-safe fashion
	 */
	public ZeroConfig(Lock lock){
		this.lock = lock;
		lock.lock();
		try {
			
			log.info("**************Initializing ZeroConfig**************");
			initProvider();
			
			File keypassFile = new File(getKeyMaterialsBase(),".keypass");
			log.debug("Keypassfile: "+keypassFile);
			if(!keypassFile.exists()){
				keyPass = createRandomPassword();
				log.info("keypass file does not exist, running initPasswordFile");
				initPasswordFile(".keypass",keyPass);
			}else{
				keyPass = readPasswordFile(".keypass");
				if(keyPass != null) log.debug("read keypass OK");
				else {
					log.error("keypass is null!");
				}
			}
			File storepassFile = new File(getKeyMaterialsBase(),".storepass");
			log.debug(storepassFile);
			if(!storepassFile.exists()){
				keystorePass = createRandomPassword();
				log.info("storepassFile does not exist");
				initPasswordFile(".storepass",keystorePass);
			}else{
				keystorePass = readPasswordFile(".storepass");
				if(keystorePass != null) log.debug("Found keystorePass OK");
				else {
					log.error("keystorepass is null!");
				}
			}
			
			log.info("Read or created passwords by this point...");
		
			try {
				File keystoreFile = new File(getKeyMaterialsBase(),
						"bracketKeystore."+getKeystoreFileExtension());
				if(!keystoreFile.exists()){
					log.info("Keystore file does not exist, creating...");
					initKeystore();
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		
		}finally{
			lock.unlock();
		}
	}
	
	private void initProvider(){
		Provider [] providers = Security.getProviders();
		for(Provider p: providers){
			if(p instanceof BouncyCastleProvider){
				log.debug("Found the Bouncy Castle provider in the config");
				return;
			}
		}
		log.warn("Adding the BouncyCastle provider dynamically. You might want to install it statically into the java security configuration for better performance.");
		Security.addProvider(new BouncyCastleProvider());
	}
	
	/**
	 * Reads the first line in the file and calls trim() on that
	 * 
	 * @param filename
	 * @return
	 */
	private String readPasswordFile(String filename){
		File passwordFile = new File(this.getKeyMaterialsBase(),filename);
		if(passwordFile.exists()){
			BufferedReader breader = null;
			try {
				FileReader reader = new FileReader(passwordFile);
				breader = new BufferedReader(reader);
				String val = breader.readLine().trim();
				return val;
			} catch (IOException e) {
				e.printStackTrace();
			}finally{
				if(breader != null)
					try {
						breader.close();
					} catch (IOException e) {}
			}
		}else{
			throw new RuntimeException("File "+filename+" does not exist");
		}
		
		return null;
	}
	
	/**
	 * Use the RandomeWords module to create password values 
	 * 
	 */
	private String createRandomPassword() {
		
		if(randomWords == null) {
			log.info("Using the default random word source");
			randomWords = RandomWords.Factory.defaultImpl();
			randomWords.addWordFilter(new StrengthenPasswordWordFilter());
			randomWords.generate();
		}
		
		return randomWords.getRandomSelection();
	}
	
	private void initKeystore() throws NoSuchAlgorithmException, KeyStoreException, NoSuchProviderException{
		
		    log.info("Creating keystore. We'll make a self-signed RSA cert to get going...");
		    
			KeyPairGenerator keyGen = (KeyPairGenerator) KeyPairGenerator.getInstance("RSA");
			log.info("Cert key size will be: "+getAsymmetricKeySize());
		    keyGen.initialize(getAsymmetricKeySize());
		    KeyPair keypair = keyGen.genKeyPair();
		    Key privateKey = keypair.getPrivate();
		    PublicKey publicKey = keypair.getPublic();

		    log.info("Creating certificate...");
	        //
	        // this class flags as deprecated due to some methods we're not using, so ignore the error
	        //
	        X509V3CertificateGenerator  certGen = new X509V3CertificateGenerator();

	        certGen.setSerialNumber(BigInteger.valueOf(Long.valueOf(getSerialNumber())));
	        // self signed
	        certGen.setIssuerDN(getX509Principal());
	        certGen.setNotBefore(new Date(System.currentTimeMillis() - 50000));
	        certGen.setNotAfter(new Date(System.currentTimeMillis() + 31557600000L));
	        certGen.setSubjectDN(getX509Principal());
	        certGen.setPublicKey(publicKey);
	        certGen.setSignatureAlgorithm(getSignatureAlgorithm());

	        Certificate [] chain = new Certificate[1];

	        try {
	            X509Certificate cert = certGen.generate((PrivateKey)privateKey);
	            cert.checkValidity(new Date());
	            cert.verify(publicKey);
	            ByteArrayInputStream    bIn = new ByteArrayInputStream(cert.getEncoded());
	            CertificateFactory      fact = CertificateFactory.getInstance("X.509", getJCEProvider());

	            cert = (X509Certificate)fact.generateCertificate(bIn);
	            log.info("Cert DN is: "+cert.getSubjectDN().getName());
	            log.info("Cert Not After: "+cert.getNotAfter());
	            chain[0] = (Certificate) cert;
	        }catch (Exception e){
	            e.printStackTrace();
	        }

	        KeyStore store = KeyStore.getInstance(getKeystoreType(), getJCEProvider());
	        log.info("Out store is of type "+store.getClass());

	        try {
				store.load(null, null);
				log.info("Store was loaded with store.load(null, null), documented way to do that");
			} catch (CertificateException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
	        
	        log.info("Creating private key entry, alias = "+getAlias());

	        store.setKeyEntry(getAlias(), privateKey, getKeypass().toCharArray(), chain);
			File keystoreFile = new File(getKeyMaterialsBase(),"bracketKeystore."+this.getKeystoreFileExtension());
	       try {
			  log.info("keystoreName = "+keystoreFile.getCanonicalPath());
		   } catch (IOException e1) {}
	       
			FileOutputStream bOut=null;
			try {
				bOut = new FileOutputStream(keystoreFile);
				 try {
					store.store(bOut, getStorepass().toCharArray());
					log.info("Writing keystore with our data.");
				} catch (CertificateException e) {
					e.printStackTrace();
				} catch (IOException e) {
					e.printStackTrace();
				}
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			}finally{
				if(bOut!=null)
					try {
						bOut.close();
					} catch (IOException e) {}
			}

			log.info("*******Keystore created, self-signed cert in place***********");
	       
	}
	
	private void initPasswordFile(String fileName, String password){
	
		File passFile = new File(getKeyMaterialsBase(),fileName);
		FileWriter writer = null;
		try {
			writer = new FileWriter(passFile);
			writer.append(password);
			writer.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}finally{
			if(writer != null)
				try {
					writer.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
		}
	}

	public String getKeypass() {
		return keyPass;
	}

	public String getStorepass() {
		return keystorePass;
	}

	public String getKeystoreType() {
		return "UBER";
	}
	
	public String getKeystoreFileExtension() {
		return "ubr";
	}

	public String getSignatureAlgorithm() {
		return "SHA256withRSA";
	}

	public String getJCEProvider() {
		return "BC";
	}

	public String getSymmetricAlgorithm() {
		return "AES";
	}

	public int getSymmetricKeySize() {
		return 128;
	}
	
	public int getAsymmetricKeySize() {
		return 2048;
	}

	public String getAsymmetricAlgorithm() {
		//return "RSA/NONE/PKCS1Padding";
		return "RSA/ECB/PKCS1Padding";
		//return "RSA/NONE/NoPadding";
	}

	public boolean useCaching() {
		return true;
	}

	public File getKeyMaterialsBase() {
		lock.lock();
		try {
			File file = new File(System.getProperty("user.home"));
			File parent = new File(file,"bracket");
			parent.mkdirs();
			return parent;
		}finally{
			lock.unlock();
		}
	}
	
	public String getSerialNumber(){
		return "1";
	}

	public String getSubjectDN() {
		return "C=AU,O=Bracket Projects,L=MacMasters Beach,ST=New South Wales";
	}
	
	public String getAlias(){
		return "private-key";
	}
	
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private X509Principal getX509Principal(){
		
			Hashtable attrs = new Hashtable();
			Vector order = new Vector();
        
			String [] array = getSubjectDN().split(",");
			for(String pair : array){
				String [] parts = pair.split("=");
				if(parts[0].equals("C")){
					  attrs.put(X509Principal.C, parts[1]);
					  order.addElement(X509Principal.C);
					  continue;
				}else if(parts[0].equals("O")){
					  attrs.put(X509Principal.O, parts[1]);
					  order.addElement(X509Principal.O);
					  continue;
				}else if(parts[0].equals("ST")){
					  attrs.put(X509Principal.ST, parts[1]);
					  order.addElement(X509Principal.ST);
					  continue;
				}else if(parts[0].equals("OU")){
					  attrs.put(X509Principal.OU, parts[1]);
					  order.addElement(X509Principal.OU);
					  continue;
				}else if(parts[0].equals("E")){
					  attrs.put(X509Principal.E, parts[1]);
					  order.addElement(X509Principal.E);
					  continue;
				}else if(parts[0].equals("L")){
					  attrs.put(X509Principal.L, parts[1]);
					  order.addElement(X509Principal.L);
					  continue;
				}
			}
	        
	        return new X509Principal(order, attrs);
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((keyPass == null) ? 0 : keyPass.hashCode());
		result = prime * result
				+ ((keystorePass == null) ? 0 : keystorePass.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		ZeroConfig other = (ZeroConfig) obj;
		if (keyPass == null) {
			if (other.keyPass != null)
				return false;
		} else if (!keyPass.equals(other.keyPass))
			return false;
		if (keystorePass == null) {
			if (other.keystorePass != null)
				return false;
		} else if (!keystorePass.equals(other.keystorePass))
			return false;
		return true;
	}

}
